AWS CDKでWindows Updateへの通信のみ許可するNetwork Firewallを設定してみた
はじめに
こんにちは、のんピ です。
皆さんはプロキシサーバの運用をしたことありますか? 私はあります。 私はプロキシサーバの運用をする時に以下のようなところに苦しめられました。
- プロキシサーバの可用性を担保するのが大変
- ドメインフィルタリング機能のルールの設定が大変
今までプロキシサーバを構築する際は、EC2インスタンス内にミドルウェアをインストールされていたパターンが多いかと思いますが、 昨年、ドメインフィルタリングができるAWS Network Firewallという、AWSのマネージドサービスがGAになりました。
また、「NAT Gatewayは使いたいけど、プライベートサブネットにあるEC2インスタンスから必要以上にインターネットへの通信は制限したい」といった要望は受けたことはありませんか? 私はあります。
この記事では、そういったプロキシサーバ運用のつらみと、NAT Gatewayのつらみを、Network Firewallを使って解決していきたいと思います。
今回の検証の想定としては、Windows Updateは実施したいけど、Windows Update以外のインターネットに抜ける通信はさせたくない
といった用件で検証をしていきます。
いきなりまとめ
- SSM Patch Managerだろうと、SSM RunCommandだろうと、Windows Updateをする際にはインターネットへの経路が必要。
- NAT Gatewayと組み合わせれば、プライベートサブネットのEC2インスタンスもドメインフィルタリングすることができる。
- Reachability Analyzer上から経路の到達確認をすると、EC2インスタンスからインターネットへの経路は到達不能になる。
- Network Firewallは結構お金がかかる。
今回の検証の構成
今回の検証の構成は以下図の通りです。 2つのAZにNetwork Firewall用のFirewallサブネット、NAT GatewayがいるPublicサブネット、EC2インスタンスがいるPrivateサブネットがあります。
その他、構成図には記載していませんが以下のようにVPCエンドポイントも作成しています。
- EC2インスタンスでSSMのセッションマネージャーをするために、以下VPCエンドポイントを作成している
- ssm
- ssm-messages
- ec2
- ec2-messages
- s3
- EC2インスタンスのイベントログやメトリクスを取得するために以下VPCエンドポイントを作成している
- monitoring
- logs
Network Firewall自体の細かい解説は以下の記事が参考になるかと思います。
やってみた
CDK周りのコードの確認
私は手動で真心を込めて構築するタイプではないので、AWS CDK
でリソースをデプロイしていきたいと思います。
CDKを実行して、作成されるリソースは図の赤枠の箇所です。
CDKでリソースを作成するにあたって、こちらのGitHubが大変参考になりました。 CDKのメインのコードは以下の通りです。
import * as cdk from "@aws-cdk/core"; import * as ec2 from "@aws-cdk/aws-ec2"; import * as logs from "@aws-cdk/aws-logs"; import * as iam from "@aws-cdk/aws-iam"; import * as networkfirewall from "@aws-cdk/aws-networkfirewall"; import * as ssm from "@aws-cdk/aws-ssm"; import * as fs from "fs"; export class AppStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // Create CloudWatch Logs for VPC Flow Logs const flowLogsLogGroup = new logs.LogGroup(this, "FlowLogsLogGroup", { retention: logs.RetentionDays.ONE_WEEK, }); // Create CloudWatch Logs for Network Firewall Logs const networkFirewallFlowLogsLogGroup = new logs.LogGroup( this, "NetworkFirewallFlowLogsLogGroup", { retention: logs.RetentionDays.ONE_WEEK, } ); // Create VPC Flow Logs IAM role const flowLogsIamrole = new iam.Role(this, "FlowLogsIamrole", { assumedBy: new iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"), }); // Create SSM IAM role const ssmIamRole = new iam.Role(this, "SsmIamRole", { assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( "AmazonSSMManagedInstanceCore" ), iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMPatchAssociation"), iam.ManagedPolicy.fromAwsManagedPolicyName( "CloudWatchAgentAdminPolicy" ), ], }); // Create VPC Flow Logs IAM Policy const flowLogsIamPolicy = new iam.Policy(this, "FlowLogsIamPolicy", { statements: [ new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["iam:PassRole"], resources: [flowLogsIamrole.roleArn], }), new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogStreams", ], resources: [flowLogsLogGroup.logGroupArn], }), ], }); // Atach VPC Flow Logs IAM Policy flowLogsIamrole.attachInlinePolicy(flowLogsIamPolicy); // Create VPC const vpc = new ec2.Vpc(this, "Vpc", { cidr: "10.0.0.0/16", enableDnsHostnames: true, enableDnsSupport: true, natGateways: 2, maxAzs: 2, subnetConfiguration: [ { name: "Firewall", subnetType: ec2.SubnetType.PRIVATE, cidrMask: 28 }, { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 }, { name: "Private", subnetType: ec2.SubnetType.PRIVATE, cidrMask: 24 }, ], }); // Setting VPC Flow Logs new ec2.CfnFlowLog(this, "FlowLogToLogs", { resourceId: vpc.vpcId, resourceType: "VPC", trafficType: "ALL", deliverLogsPermissionArn: flowLogsIamrole.roleArn, logDestination: flowLogsLogGroup.logGroupArn, logDestinationType: "cloud-watch-logs", logFormat: "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}", maxAggregationInterval: 60, }); // Create SSM Privatelink new ec2.InterfaceVpcEndpoint(this, "SsmVpcEndpoint", { vpc: vpc, service: ec2.InterfaceVpcEndpointAwsService.SSM, subnets: vpc.selectSubnets({ subnetGroupName: "Private" }), }); // Create SSM MESSAGES Privatelink new ec2.InterfaceVpcEndpoint(this, "SsmMessagesVpcEndpoint", { vpc: vpc, service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES, subnets: vpc.selectSubnets({ subnetGroupName: "Private" }), }); // Create EC2 MESSAGES Privatelink new ec2.InterfaceVpcEndpoint(this, "Ec2VpcEndpoint", { vpc: vpc, service: ec2.InterfaceVpcEndpointAwsService.EC2, subnets: vpc.selectSubnets({ subnetGroupName: "Private" }), }); // Create EC2 MESSAGES Privatelink new ec2.InterfaceVpcEndpoint(this, "Ec2MessagesVpcEndpoint", { vpc: vpc, service: ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES, subnets: vpc.selectSubnets({ subnetGroupName: "Private" }), }); // Create CloudWatch Privatelink new ec2.InterfaceVpcEndpoint(this, "CloudwatchVpcEndpoint", { vpc: vpc, service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH, subnets: vpc.selectSubnets({ subnetGroupName: "Private" }), }); // Create CloudWatch Privatelink new ec2.InterfaceVpcEndpoint(this, "CloudwatchLogsVpcEndpoint", { vpc: vpc, service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS, subnets: vpc.selectSubnets({ subnetGroupName: "Private" }), }); // // Create S3 Gateway new ec2.GatewayVpcEndpoint(this, "S3GatewayVpcEndpoint", { vpc: vpc, service: ec2.GatewayVpcEndpointAwsService.S3, }); // Get Network Firewall Subnet ID const firewallSubnetId = new Array(); vpc .selectSubnets({ subnetGroupName: "Firewall" }) .subnets.forEach((subnet) => { firewallSubnetId.push({ subnetId: subnet.subnetId }); }); // Create EC2 instance vpc .selectSubnets({ subnetGroupName: "Private" }) .subnets.forEach((subnet, index) => { new ec2.Instance(this, `Ec2Instance${index}`, { machineImage: ec2.MachineImage.latestWindows( ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_BASE ), instanceType: new ec2.InstanceType("t3.micro"), vpc: vpc, keyName: this.node.tryGetContext("key-pair"), role: ssmIamRole, vpcSubnets: vpc.selectSubnets({ subnetGroupName: "Private" }), }); }); // Create Network Firewall rule group const networkfirewallRuleGroup = new networkfirewall.CfnRuleGroup( this, "NetworkFirewallRuleGroup", { capacity: 100, ruleGroupName: "WindowsUpdateRuleGroup", type: "STATEFUL", ruleGroup: { rulesSource: { rulesSourceList: { generatedRulesType: "ALLOWLIST", targetTypes: ["TLS_SNI", "HTTP_HOST"], targets: [ ".update.microsoft.com", ".download.microsoft.com", ".windowsupdate.com", ".windowsupdate.microsoft.com", ".mp.microsoft.com", "wustat.windows.com", "ntservicepack.microsoft.com", "login.live.com", ], }, }, }, } ); // Create Network Firewall policy const networkfirewallPolicy = new networkfirewall.CfnFirewallPolicy( this, "NetworkFirewallPolicy", { firewallPolicyName: "WindowsUpdatePolicy", firewallPolicy: { statelessDefaultActions: ["aws:forward_to_sfe"], statelessFragmentDefaultActions: ["aws:forward_to_sfe"], statefulRuleGroupReferences: [ { resourceArn: networkfirewallRuleGroup.attrRuleGroupArn, }, ], }, } ); // Create Network Firewall const networkFirewall = new networkfirewall.CfnFirewall( this, "NetworkFirewall", { firewallName: "NetworkFirewall", firewallPolicyArn: networkfirewallPolicy.attrFirewallPolicyArn, vpcId: vpc.vpcId, subnetMappings: firewallSubnetId, } ); // // Setting Network Firewall logs new networkfirewall.CfnLoggingConfiguration( this, "NetworkFirewallFlowLogsToLogs", { firewallArn: networkFirewall.ref, loggingConfiguration: { logDestinationConfigs: [ { logDestination: { logGroup: networkFirewallFlowLogsLogGroup.logGroupName, }, logDestinationType: "CloudWatchLogs", logType: "FLOW", }, ], }, } ); // Routing NAT Gateway to Network Firewall vpc .selectSubnets({ subnetGroupName: "Public" }) .subnets.forEach((subnet, index) => { const route = subnet.node.children.find( (child) => child.node.id == "DefaultRoute" ) as ec2.CfnRoute; route.addDeletionOverride("Properties.GatewayId"); route.addOverride( "Properties.VpcEndpointId", cdk.Fn.select( 1, cdk.Fn.split( ":", cdk.Fn.select(index, networkFirewall.attrEndpointIds) ) ) ); }); // Routing Network Firewall to Internet Gateway vpc .selectSubnets({ subnetGroupName: "Firewall" }) .subnets.forEach((subnet, index) => { const route = subnet.node.children.find( (child) => child.node.id == "DefaultRoute" ) as ec2.CfnRoute; route.addDeletionOverride("Properties.NatGatewayId"); route.addOverride("Properties.GatewayId", vpc.internetGatewayId); }); // Internet Gateway RouteTable const igwRouteTable = new ec2.CfnRouteTable(this, "IgwRouteTable", { vpcId: vpc.vpcId, }); // Routing Internet Gateway to Network Firewall vpc .selectSubnets({ subnetGroupName: "Public" }) .subnets.forEach((subnet, index) => { const igwRouteToFirewall = new ec2.CfnRoute( this, `IgwRouteTableToFirewall${index}`, { routeTableId: igwRouteTable.ref, destinationCidrBlock: subnet.ipv4CidrBlock, vpcEndpointId: cdk.Fn.select( 1, cdk.Fn.split( ":", cdk.Fn.select(index, networkFirewall.attrEndpointIds) ) ), } ); }); // Association Internet Gateway RouteTable new ec2.CfnGatewayRouteTableAssociation(this, "IgwRouteTableAssociation", { gatewayId: <string>vpc.internetGatewayId, routeTableId: igwRouteTable.ref, }); // CloudWatch parameters for Windows OS const stringValue = fs.readFileSync( "./AmazonCloudWatch-windows.json", "utf8" ); // Create a new SSM Parameter new ssm.StringParameter(this, "StringParameter", { description: "CloudWatch parameters for Windows OS", parameterName: "AmazonCloudWatch-windows", stringValue: stringValue, }); } }
CDKのポイント
VPC Flow Logs
VPC Flow LogsのコードってL2で書けるよね?と思った方。その通りです。
L2で書けば。以下のように簡潔に書くことができます。
new ec2.FlowLog(this, "FlowLogToLogs", { resourceType: ec2.FlowLogResourceType.fromVpc(vpc), destination: ec2.FlowLogDestination.toCloudWatchLogs(flowLogsLogGroup), });
今回L2ではなく、以下のようにL1で記載した理由はログのフォーマットや、ログの収集間隔をカスタマイズするためです。
// Setting VPC Flow Logs new ec2.CfnFlowLog(this, "FlowLogToLogs", { resourceId: vpc.vpcId, resourceType: "VPC", trafficType: "ALL", deliverLogsPermissionArn: flowLogsIamrole.roleArn, logDestination: flowLogsLogGroup.logGroupArn, logDestinationType: "cloud-watch-logs", logFormat: "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}", maxAggregationInterval: 60, });
AWS公式のCDKのリファレンスでL2でVPC Flow Logsを設定する箇所を確認すると、ログのフォーマットや、ログの収集間隔に関するプロパティが見つかりません。
デフォルトではログの収集間隔は10分間であるため、ログが出力されるのに10分待つ必要があり、ログのレベルも物足りない状態になります。 特に私はとてもせっかちなので、10分はとても待てないと思い、L1で記載しました。
L1で記載する際のコツとしては、CloudFormationのリファレンスを隣に置いておくことだと思います。
例えば、VPC Flow LogsをL1で記述する際は、CfnFlowLog
というクラスを使います。コーディングするためにリファレンスを開くと、CfnFlowLog
クラス内のプロパティは以下のように型名
とCloudFormationで言う所のこのプロパティだよ!
というところしか記載されていません。
そのため、Stringで書けば良いのは分かったけど、具体的にどんな値を書けば良いのか分からん状態になります。
でも安心してください。CloudFormationで言う所のこのプロパティだよ!
のという通り、CloudFormationにヒントが書かれています。
実際に、CloudFormationのリファレンスを開くと、プロパティの詳細な説明が記載されています。
今回の場合、ResourceId
には、VPC Flow logsを取得する元の、サブネットもしくは、ENI、VPCのIDを記述すれば良いことが分かります。
このようにL1で書く必要があるタイミングがある際は、リファレンスを上手く活用すると、詰まらずに設定できると思います。
Network Firewallのルール
Windows Updateの通信先はMicrosoftのドキュメントに記載があります。 通信先を列挙すると以下の通りです。
- http://windowsupdate.microsoft.com
- http://*.windowsupdate.microsoft.com
- https://*.windowsupdate.microsoft.com
- http://*.update.microsoft.com
- https://*.update.microsoft.com
- http://*.windowsupdate.com
- http://download.windowsupdate.com
- https://download.microsoft.com
- http://*.download.windowsupdate.com
- http://wustat.windows.com
- http://ntservicepack.microsoft.com
- http://go.microsoft.com
- http://dl.delivery.mp.microsoft.com
- https://dl.delivery.mp.microsoft.com
Network Firewallは.
がワイルドカードとして使えるので、上述した通信先から省略して書くことができます。
Domain list – List of strings specifying the domain names that you want to match. A packet must match one of the domain specifications in the >list to be a match for the rule group. Valid domain name specifications are the following:
Explicit names. For example, abc.example.com matches only the domain abc.example.com.
Names that use a domain wildcard, which you indicate with an initial '.'. For example,.example.com matches example.com and matches all >subdomains of example.com, such as abc.example.com and www.example.com.
Stateful domain list rule groups in AWS Network Firewall - Match settings
プロトコルも別の条件と記載することができるので、CDKで書くと以下のようになります。
// Create Network Firewall rule group const networkfirewallRuleGroup = new networkfirewall.CfnRuleGroup( this, "NetworkFirewallRuleGroup", { capacity: 100, ruleGroupName: "WindowsUpdateRuleGroup", type: "STATEFUL", ruleGroup: { rulesSource: { rulesSourceList: { generatedRulesType: "ALLOWLIST", targetTypes: ["TLS_SNI", "HTTP_HOST"], targets: [ ".update.microsoft.com", ".download.microsoft.com", ".windowsupdate.com", ".windowsupdate.microsoft.com", ".mp.microsoft.com", "wustat.windows.com", "ntservicepack.microsoft.com", "login.live.com", ], }, }, }, } );
Ingress Route Table
Network Firewallがいる影響で、Internet Gatewayにアタッチする Route Tableを編集する必要があります。
インスタンスからWindows Updateの通信は以下の通りです。
インスタンス -> NAT Gateway -> Network Firewall -> Internet Gateway -> Windows Update通信先
(構成図の赤い太い線)
問題はレスポンスの通信です。
本来のあるべきレスポンスの通信経路は、リクエストの通信と逆で、構成図の青い太い線のように、Windows Update通信先 -> Internet Gateway -> Network Firewall -> NAT Gateway -> インスタンス
です。
しかし、リクエストの通信の際にNetwork FirewallはパブリックIPアドレスを持っていないために、Windows Update通信先 -> Internet Gateway -> NAT Gateway -> インスタンス
となり、非対称なルーティングになってしまいます。
このようなことを防ぐために、Ingress Route Tableを使用します。
Ingress Route Tableを使用して、NAT Gatewayに対しての通信が来た場合は、Network Firewallのエンドポイントに向くように変更します。これでリクエスト/レスポンスで対称なルーティングとなります。
Ingress Route Tableについては以下記事が非常に参考になるかと思います。
コードの確認に戻ります。
./lib/app-stack.ts
内で読み込んでいる./AmazonCloudWatch-windows.json
は以下の通りです。
これはCloudWatch Agentインストール時に併せてインストールされるamazon-cloudwatch-agent-config-wizard.exe
の出力結果をベースにしたものです。
(なんとなくログとかメトリクスとか取りたくなるのは私だけでしょうか??)
{ "logs": { "logs_collected": { "windows_events": { "collect_list": [{ "event_format": "xml", "event_levels": [ "INFORMATION", "WARNING", "ERROR", "CRITICAL" ], "event_name": "System", "log_group_name": "/WindowsEvents/System/" }, { "event_format": "xml", "event_levels": [ "INFORMATION", "WARNING", "ERROR", "CRITICAL" ], "event_name": "Security", "log_group_name": "/WindowsEvents/Security/" }, { "event_format": "xml", "event_levels": [ "INFORMATION", "WARNING", "ERROR", "CRITICAL" ], "event_name": "Application", "log_group_name": "/WindowsEvents/Application/" } ] } } }, "metrics": { "append_dimensions": { "AutoScalingGroupName": "${aws:AutoScalingGroupName}", "ImageId": "${aws:ImageId}", "InstanceId": "${aws:InstanceId}", "InstanceType": "${aws:InstanceType}" }, "metrics_collected": { "LogicalDisk": { "measurement": [ "% Free Space" ], "metrics_collection_interval": 60, "resources": [ "*" ] }, "Memory": { "measurement": [ "% Committed Bytes In Use" ], "metrics_collection_interval": 60 }, "Paging File": { "measurement": [ "% Usage" ], "metrics_collection_interval": 60, "resources": [ "*" ] }, "PhysicalDisk": { "measurement": [ "% Disk Time", "Disk Write Bytes/sec", "Disk Read Bytes/sec", "Disk Writes/sec", "Disk Reads/sec" ], "metrics_collection_interval": 60, "resources": [ "*" ] }, "Processor": { "measurement": [ "% User Time", "% Idle Time", "% Interrupt Time" ], "metrics_collection_interval": 60, "resources": [ "*" ] }, "TCPv4": { "measurement": [ "Connections Established" ], "metrics_collection_interval": 60 }, "TCPv6": { "measurement": [ "Connections Established" ], "metrics_collection_interval": 60 }, "statsd": { "metrics_aggregation_interval": 60, "metrics_collection_interval": 10, "service_address": ":8125" } } } }
また、特に面白いものはインストールしていませんが、package.json
は以下の通りです。
今回、CDKのバージョンは1.101.0
です。
{ "name": "app", "version": "0.1.0", "bin": { "app": "bin/app.js" }, "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "cdk": "cdk" }, "devDependencies": { "@aws-cdk/assert": "1.101.0", "@types/jest": "^26.0.23", "@types/node": "10.17.27", "aws-cdk": "1.101.0", "jest": "^26.4.2", "ts-jest": "^26.2.0", "ts-node": "^9.0.0", "typescript": "~3.9.7" }, "dependencies": { "@aws-cdk/aws-ec2": "^1.101.0", "@aws-cdk/aws-iam": "^1.101.0", "@aws-cdk/aws-networkfirewall": "^1.101.0", "@aws-cdk/aws-s3": "^1.101.0", "@aws-cdk/aws-ssm": "^1.101.0", "@aws-cdk/core": "1.101.0", "fs": "0.0.1-security", "source-map-support": "^0.5.16" } }
リソースのデプロイ
CDKを使ってリソースをデプロイしていきます。私はCDKをグローバルでインストールしていないので、npx
を使用しています。
CDKをグローバルでインストールされている方は先頭のnpx
を無視して読み替えてください。
// CDKのバージョン確認 > npx cdk --version 1.101.0 (build 149f0fc) // リソースのデプロイ > npx cdk deploy This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening). Please confirm you intend to make the following modifications: IAM Statement Changes ┌───┬────────────────────────────────┬────────┬────────────────────────────────┬──────────────────────────────────┬───────────┐ │ │ Resource │ Effect │ Action │ Principal │ Condition │ ├───┼────────────────────────────────┼────────┼────────────────────────────────┼──────────────────────────────────┼───────────┤ │ + │ ${FlowLogsIamrole.Arn} │ Allow │ sts:AssumeRole │ Service:vpc-flow-logs.amazonaws. │ │ │ │ │ │ │ com │ │ │ + │ ${FlowLogsIamrole.Arn} │ Allow │ iam:PassRole │ AWS:${FlowLogsIamrole} │ │ ├───┼────────────────────────────────┼────────┼────────────────────────────────┼──────────────────────────────────┼───────────┤ │ + │ ${FlowLogsLogGroup.Arn} │ Allow │ logs:CreateLogStream │ AWS:${FlowLogsIamrole} │ │ │ │ │ │ logs:DescribeLogStreams │ │ │ │ │ │ │ logs:PutLogEvents │ │ │ ├───┼────────────────────────────────┼────────┼────────────────────────────────┼──────────────────────────────────┼───────────┤ │ + │ ${SsmIamRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.${AWS::URLSuffix} │ │ └───┴────────────────────────────────┴────────┴────────────────────────────────┴──────────────────────────────────┴───────────┘ IAM Policy Changes ┌───┬───────────────┬────────────────────────────────────────────────────────────────────┐ │ │ Resource │ Managed Policy ARN │ ├───┼───────────────┼────────────────────────────────────────────────────────────────────┤ │ + │ ${SsmIamRole} │ arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore │ │ + │ ${SsmIamRole} │ arn:${AWS::Partition}:iam::aws:policy/AmazonSSMPatchAssociation │ │ + │ ${SsmIamRole} │ arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentAdminPolicy │ └───┴───────────────┴────────────────────────────────────────────────────────────────────┘ Security Group Changes ┌───┬────────────────────────────────────────────────────┬─────┬────────────┬──────────────────┐ │ │ Group │ Dir │ Protocol │ Peer │ ├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤ │ + │ ${CloudwatchLogsVpcEndpoint/SecurityGroup.GroupId} │ In │ TCP 443 │ ${Vpc.CidrBlock} │ │ + │ ${CloudwatchLogsVpcEndpoint/SecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │ ├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤ │ + │ ${CloudwatchVpcEndpoint/SecurityGroup.GroupId} │ In │ TCP 443 │ ${Vpc.CidrBlock} │ │ + │ ${CloudwatchVpcEndpoint/SecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │ ├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤ │ + │ ${Ec2Instance0/InstanceSecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │ ├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤ │ + │ ${Ec2Instance1/InstanceSecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │ ├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤ │ + │ ${Ec2MessagesVpcEndpoint/SecurityGroup.GroupId} │ In │ TCP 443 │ ${Vpc.CidrBlock} │ │ + │ ${Ec2MessagesVpcEndpoint/SecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │ ├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤ │ + │ ${Ec2VpcEndpoint/SecurityGroup.GroupId} │ In │ TCP 443 │ ${Vpc.CidrBlock} │ │ + │ ${Ec2VpcEndpoint/SecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │ ├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤ │ + │ ${SsmMessagesVpcEndpoint/SecurityGroup.GroupId} │ In │ TCP 443 │ ${Vpc.CidrBlock} │ │ + │ ${SsmMessagesVpcEndpoint/SecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │ ├───┼────────────────────────────────────────────────────┼─────┼────────────┼──────────────────┤ │ + │ ${SsmVpcEndpoint/SecurityGroup.GroupId} │ In │ TCP 443 │ ${Vpc.CidrBlock} │ │ + │ ${SsmVpcEndpoint/SecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │ └───┴────────────────────────────────────────────────────┴─────┴────────────┴──────────────────┘ (NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299) Do you wish to deploy these changes (y/n)? y AppStack: deploying... AppStack: creating CloudFormation changeset... [██████████████████████████████████████████████████████████] (67/67) ✅ AppStack Stack ARN: arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/AppStack/47c7a540-a956-11eb-a52f-1245efb4d2e3
リソースの確認
ここで一旦デプロイされたリソースを確認してみます。
サブネットを確認してみると、以下のようにキチンと各AZにデプロイされていることが分かります。
Ingress Route Tableも確認してみると、正しく、NAT Gateway行きだった場合は、Network Firewallのエンドポイントルーティングするようになっています。
Network Firewallも確認してみます。 Network Firewallのルールを確認すると、CDKで記載した通りにドメインとプロトコルが許可されています。
また、Reachability Analyzerでインスタンスからインターネットに通信できるか確認してみます。
送信元
をインスタンス、送信先をInternet Gateway
を指定すると、以下のように到達不可能
と出力されます。
Privateサブネット、Publicサブネット、Firewallサブネットと正しくルートテーブルは設定していますが、Firewall EndpointがNAT GatewayとInternet Gatewayの間に存在すると正しく経路が認識されない挙動のようです。挙動としては不思議ですが、このまま作業を継続します。
動作確認
SSMセッションマネージャーでインスタンスに接続して、以下URLにアクセスしてみました。
- https://dev.classmethod.jp
- http://www.google.com/
- http://update.microsoft.com
- http://windowsupdate.microsoft.com
1~2の2つのURLにアクセスすると拒否され、3~4のURLは正常に通信できる想定です。 結果は以下の通り、Windows Updateで使用されるURLへの通信は許可され、許可されていないURLへの通信はタイムアウトになることが確認できました。
Windows PowerShell Copyright (C) 2014 Microsoft Corporation. All rights reserved. PS C:\Windows\system32> Invoke-WebRequest https://dev.classmethod.jp -UseBasicParsing Invoke-WebRequest : 接続が切断されました: 送信時に、予期しないエラーが発生しました。。 発生場所 行:1 文字:1 + Invoke-WebRequest https://dev.classmethod.jp -UseBasicParsing + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebException + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand PS C:\Windows\system32> Invoke-WebRequest http://www.google.com/ -UseBasicParsing Invoke-WebRequest : 接続が切断されました: 受信時に予期しないエラーが発生しました。 発生場所 行:1 文字:1 + Invoke-WebRequest http://www.google.com/ -UseBasicParsing + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebException + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand PS C:\Windows\system32> Invoke-WebRequest http://update.microsoft.com -UseBasicParsing StatusCode : 200 StatusDescription : OK Content : <html> <head> <!-- Mimic Internet Explorer 7 --> <meta http-equiv='X-UA-Compatible' content='IE=5; requiresActiveX=true' /> <meta http-equiv="PICS-Label" content='(PICS-1.1 "http://www.r... RawContent : HTTP/1.1 200 OK Vary: * Content-Length: 2048 Cache-Control: public, max-age=0 Content-Type: text/html; charset=utf-8 Date: Thu, 29 Apr 2021 07:44:22 GMT Expires: Thu, 29 Apr 2021 07:44:23 GMT L... Forms : Headers : {[Vary, *], [Content-Length, 2048], [Cache-Control, public, max-age=0], [Content-Type, text/html; charset=utf-8]...} Images : {} InputFields : {} Links : {} ParsedHtml : RawContentLength : 2048 PS C:\Windows\system32> Invoke-WebRequest http://windowsupdate.microsoft.com -UseBasicParsing StatusCode : 200 StatusDescription : OK Content : <html> <head> <!-- Mimic Internet Explorer 7 --> <meta http-equiv='X-UA-Compatible' content='IE=5; requiresActiveX=true' /> <meta http-equiv="PICS-Label" content='(PICS-1.1 "http://www.r... RawContent : HTTP/1.1 200 OK Vary: * Content-Length: 2048 Cache-Control: public, max-age=56 Content-Type: text/html; charset=utf-8 Date: Thu, 29 Apr 2021 07:44:28 GMT Expires: Thu, 29 Apr 2021 07:45:25 GMT ... Forms : Headers : {[Vary, *], [Content-Length, 2048], [Cache-Control, public, max-age=56], [Content-Type, text/html; charset=utf-8]...} Images : {} InputFields : {} Links : {} ParsedHtml : RawContentLength : 2048 PS C:\Windows\system32>
VPC Flow Logsで通信の様子を確認してみましょう。 NAT GatewayのENIのVPC Flow Logsを確認すると以下のようになっていました。
| ${version} | ${account-id} | ${interface-id} | ${srcaddr} | ${dstaddr} | ${srcport} | ${dstport} | ${protocol} | ${packets} | ${bytes} | ${start} | ${end} | ${action} | ${log-status} | ${vpc-id} | ${subnet-id} | ${instance-id} | ${tcp-flags} | ${type} | ${pkt-srcaddr} | ${pkt-dstaddr} | ${region} | ${az-id} | ${sublocation-type} | ${sublocation-id} | ${pkt-src-aws-service} | ${pkt-dst-aws-service} | ${flow-direction} | ${traffic-path} | | ---------- | ------------- | --------------------- | ---------- | ---------- | ---------- | ---------- | ----------- | ---------- | -------- | ---------- | ---------- | --------- | ------------- | --------------------- | ------------------------ | -------------- | ------------ | ------- | -------------- | -------------- | --------- | -------- | ------------------- | ----------------- | ---------------------- | ---------------------- | ----------------- | --------------- | | 5 | <AWSアカウントID>| eni-08e9a808001d52a42 | 10.0.1.17 | 10.0.0.8 | 55430 | 80 | 6 | 2 | 80 | 1619748444 | 1619748453 | ACCEPT | OK | vpc-015f0a3a497792ace | subnet-01e159902b87d0c83 | - | 1 | IPv4 | 10.0.1.17 | 52.137.90.34 | us-east-1 | use1-az6 | - | - | - | - | ingress | - | | 5 | <AWSアカウントID>| eni-08e9a808001d52a42 | 10.0.0.8 | 10.0.1.17 | 80 | 55430 | 6 | 1 | 40 | 1619748444 | 1619748453 | ACCEPT | OK | vpc-015f0a3a497792ace | subnet-01e159902b87d0c83 | - | 1 | IPv4 | 52.137.90.34 | 10.0.1.17 | us-east-1 | use1-az6 | - | - | - | - | egress | 1 |
3行目の${dstaddr}
と、${pkt-dstaddr}
を見比べてると分かる通り、送信先のIPアドレスが変換されてます。
本来の送信元(${pkt-dstaddr}
)は、52.137.90.34
となっています。このIPアドレスを逆引きすると、Microsoftがゾーン情報を管理していることからWindows Updateに使用されるIPアドレスであることが分かります。
> dig -x 52.137.90.34 ; <<>> DiG 9.10.6 <<>> -x 52.137.90.34 ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 35412 ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;34.90.137.52.in-addr.arpa. IN PTR ;; AUTHORITY SECTION: 90.137.52.in-addr.arpa. 60 IN SOA prd1.azuredns-cloud.net. msnhst.microsoft.com. 1 900 300 604800 60
${dstaddr}
の10.0.0.8
ですが、このIPアドレスはNetwork FirewallのVPCエンドポイントのIPアドレスです。そのため、Network Firewallにルーティングするために、AWS側でIPアドレスを書き換われていることが分かります。
このことから、3行目がEC2インスタンスからWindows UpdateへのURLへのリクエストの通信で、4行目がそのレスポンスの通信であることが分かります。
また、Network FirewallのENIのVPC Flow Logsを確認すると以下のようになっていました。
| ${version} | ${account-id} | ${interface-id} | ${srcaddr} | ${dstaddr} | ${srcport} | ${dstport} | ${protocol} | ${packets} | ${bytes} | ${start} | ${end} | ${action} | ${log-status} | ${vpc-id} | ${subnet-id} | ${instance-id} | ${tcp-flags} | ${type} | ${pkt-srcaddr} | ${pkt-dstaddr} | ${region} | ${az-id} | ${sublocation-type} | ${sublocation-id} | ${pkt-src-aws-service} | ${pkt-dst-aws-service} | ${flow-direction} | ${traffic-path} | | ---------- | ------------- | --------------------- | ---------- | ------------ | ---------- | ---------- | ----------- | ---------- | -------- | ---------- | ---------- | --------- | ------------- | --------------------- | ------------------------ | -------------- | ------------ | ------- | -------------- | -------------- | --------- | -------- | ------------------- | ----------------- | ---------------------- | ---------------------- | ----------------- | --------------- | | 5 | <AWSアカウントID>| eni-08e9a808001d52a42 | 10.0.0.8 | 52.137.90.34 | 55430 | 80 | 6 | 4 | 332 | 1619748319 | 1619748324 | ACCEPT | OK | vpc-015f0a3a497792ace | subnet-01e159902b87d0c83 | - | 2 | IPv4 | 10.0.1.17 | 52.137.90.34 | us-east-1 | use1-az6 | - | - | - | - | egress | 8 |
${dstaddr}
を確認すると、52.137.90.34
となっています。このことから、Network Firewallを通過する際には、NAT Gateway通過時に変更されたIPアドレスが、本来の宛先IPアドレスに戻っていることが分かります。
このように、Network Firewallや、Gateway Load Balancerなどネットワークの通信経路に影響を与えるサービスを使用する際は、VPC Flow Logsの内容は詳細に表示するようにしておいた方が良いかと思います。
VPC Flow Logsで取得可能なフィールドについては、AWS公式ドキュメントに記載があります。
続いて、Network Firewallのモニタリングも確認してみます。ご覧の通り、どの程度の通信を許可したのか、拒否したのかが分かりやすく表示されています。
Network Firewallのログは以下の通りです。JSON形式で保存されるため、VPC Flow Logsよりも見やすいかもしれませんね。
CloudWatch Agentのインストール・設定
作成したEC2インスタンスにCloudWatch Agentをインストールしてイベントログや、メモリなどのメトリクスを取得します。
まずは、CloudWatch Agentのインストールです。
インストールはSSM RunCommandを使用して行います。
SSMのコンソールより、RunCommand
を選択して、Run command
をクリックします。
CloudWatch AgentをEC2インスタンスにインストールするため、実行するドキュメントはAWS-ConfigureAWSPackage
です。AWS-ConfigureAWSPackage
は指定されたパッケージをEC2インストール/アンインストールする際に使用されるドキュメントです。
パラメーターも以下の通り、Action: Install
、Name: AmazonCloudWatchAgent
と指定します。
実行対象のEC2インスタンスと、ログの出力先を指定します。ログの出力先はCloudWatch Logsの場合、/aws/ssm/AWS-ConfigureAWSPackage
というロググループに出力されます。
問題がなければ、右下の実行
をクリックすると、CloudWatch Agentのインストールが始まります。
実行が完了すると以下のような画面になります。どちらのEC2インスタンスもステータスが成功
になっており、全体的なステータスも成功
となっています。
RunCommandを実行した際に指定したCloudWatch Logsにログが出力されているので、確認してみます。
Successfully installed arn:aws:ssm:::package/AmazonCloudWatchAgent 1.247347.6b250880
となっているので、正常にインストールされていることを確認できます。
続いて、CloudWatch Agentの設定をしていきます。
まずは、CDKで設定したSSM Parameter storeに、CloudWatchの設定JSONが保存されているか確認します。 パラメーターストアを確認すると、意図したJSONが保存されていることが確認できました。
CloudWatch Agentの設定もRunCommandで行います。
以下のように入力します。Option Restart: yes
とすることで、設定を更新するためにCloudWatch Agentを再起動してくれます。
実行対象のEC2インスタンスと、ログの出力先の指定は、CloudWatch Agentをインストールした時と同様です。実行
をクリックすると、CloudWatch Agentの設定が開始されます。
実行が完了すると以下のような画面になります。どちらのEC2インスタンスもステータスが成功
になっており、全体的なステータスも成功
となっています。
RunCommandを実行した際に指定したCloudWatch Logsにログが出力されているので、確認してみます。
Successfully fetched the config and saved in C:\ProgramData\Amazon\AmazonCloudWatchAgent\Configs\ssm_AmazonCloudWatch-windows.tmp
とあることからSSMパラメーターストアからJSONをダウンロードできていることが分かります。
その後Configuration validation succeeded
と設定の検証もされ、サービスの再起動も行われているとなっているので、正常に設定されたことを確認できます。
それではイベントログや、メトリクスが取得できているか確認します。まずはWindowsのイベントログの確認からです。
設定の通り、/WindowsEvents/.*/
の形式でロググループが作成されているようです。
Systemイベントのログを確認すると以下の通り、正常に出力されていることが確認できました。
イベントログが取得できていることが確認できたので、続いてメトリクスです。
CloudWatchメトリクスを確認すると、カスタム名前空間としてCWAgent
が追加されています。
デプロイされた2つのEC2インスタンスのMemory % Committed Bytes In Use
を選択すると、きれいにメモリの使用率のメトリクスが表示されていました。
SSM Patch ManagerによるWindows Update
それでは、SSM Patch Managerを使ってWindows Updateをしていきます。
SSM Patch Manager自体の詳細な説明については以下記事が参考になるかと思います。
まずは、Windows Updateをするにあたって、実行するパッチベースライン
を指定していきます。今回はAWS-WindowsPredefinedPatchBaseline-OS
をパッチベースラインとして設定します。
AWS-WindowsPredefinedPatchBaseline-OS
の説明は以下、公式ドキュメントにある通り、重要度の高いパッチを適用するものになります。
分類が「CriticalUpdates」または「SecurityUpdates」で、MSRC 重要度が「非常事態」または「重要」のすべての Windows Server オペレーティングシステムパッチを承認します。パッチはリリースから 7 日後に自動承認されます。
続いてパッチグループの設定をしていきます。パッチグループの変更
をクリックします。
パッチグループ名を入力します。今回は対象のEC2インスタンスがWindows Server 2012 R2だったので、Windows Server 2012 R2
にしました。
変更後は閉じる
をクリックします。
パッチグループ名をWindows Server 2012 R2
にしたので、対象のEC2インスタンスの2台にPatch Group: Windows Server 2012 R2
のタグを付与します。
続いて、実際にWindows Updateを実施していきます。 適用するインスタンスや、スケジュール、操作を指定しています。
- パッチを適用するインスタンスは先ほど作成した
Windows Server 2012 R2
を指定します。 - パッチ適用スケジュールは今回一度だけ実行したいので、
スケジュール作成とインスタンスへのバッチ適用をスキップする
を指定します。 - パッチ適用操作はインストールまで実施して欲しいので、
スキャンとインストール
を指定します。
問題がなければ、パッチ適用を設定
をクリックします。
パッチマネージャーを確認すると、AWS-RunPatchBaseline
のStatusがInProgress
になっています。
しばらく待つと、StatusがSuccess
になっており、Windows Updateが完了したようです!
パッチマネージャーでInstall
をクリックすると、RunCommandの画面に遷移しました。Patch Managerも実際はRunCommandが動いているようです。
次にログの確認をします。
対象のインスタンスを選択して、出力の表示
をクリックすると以下のような画面に遷移します。
画面では見切れていたので、ログを以下に記載します。
InstalledCount: 42
となっていることから、パッチベースラインで定義された42個のパッチがインスタンスにインストールされているようです。
Preparing to download PatchBaselineOperations PowerShell module from S3. Downloading PatchBaselineOperations PowerShell module from https://s3.amazonaws.com/aws-ssm-us-east-1/patchbaselineoperations/Amazon.PatchBaselineOperations-1.31.zip to C:\ProgramData\Amazon\SSM\InstanceData\i-0f027dd30cbffa0cc\document\orchestration\4bb232d5-ad74-4ec7-8195-202a01af4a40\PatchWindows\Amazon.PatchBaselineOperations-1.31.zip. Extracting PatchBaselineOperations zip file contents to temporary folder. Verifying SHA 256 of the PatchBaselineOperations PowerShell module files. Successfully downloaded and installed the PatchBaselineOperations PowerShell module. Patch Summary for i-0f027dd30cbffa0cc PatchGroup : Windows Server 2012 R2 BaselineId : pb-096d816473f2bdb03 Baseline : {"AccountId":"075727635805","BaselineId":"pb-096d816473f2bdb03","Name":"AWS-WindowsPredefinedPatchBaseline-OS","GlobalFilters":{"Filters":[{"Key":"PRODUCT","Values":["*"]}]},"ApprovalRules":{"Rules":[{"ApproveAfterDays":7,"FilterGroup":{"Filters":[{"Key":"PATCH_SET","Values":["OS"]},{"Key":"CLASSIFICATION","Values":["CriticalUpdates","SecurityUpdates"]},{"Key":"MSRC_SEVERITY","Values":["Critical","Important"]}]}}]},"ApprovedPatches":[],"RejectedPatches":[],"RejectedPatchesAction":"ALLOW_AS_DEPENDENCY","CreatedTime":1557253943.887,"ModifiedTime":1557253943.887,"Description":"Approves all Windows Server operating system patches that are classified as CriticalUpdates or SecurityUpdates and that have an MSRC severity of Critical or Important. Patches are auto-approved seven days after release."} SnapshotId : 859a9f98-70b5-4a51-a7d7-0eb9af0e4999 ExecutionId : 4bb232d5-ad74-4ec7-8195-202a01af4a40 RebootOption : RebootIfNeeded OwnerInformation : OperationType : Install OperationStartTime : 2021-04-29T08:40:12.1789065Z OperationEndTime : 2021-04-29T08:44:01.7313056Z InstalledCount : 42 InstalledRejectedCount : 0 InstalledPendingRebootCount : 0 InstalledOtherCount : 167 FailedCount : 0 MissingCount : 0 CriticalNonCompliantCount : 0 SecurityNonCompliantCount : 0 OtherNonCompliantCount : 0 NotApplicableCount : 1927 UnreportedNotApplicableCount : 0 WIN-CD0UU99DGCT - PatchBaselineOperations Installation Results - 2021-04-29T09:03:34.038 KbArticleId Installed Message ----------- ----------- -----------
リソースのお片付け
無事Network Firewallを経由してWindows Updateができたので、環境のお片付けをします。 以下のコマンドでCDKでデプロイされたリソースは削除されます。なお、CloudWatch Logsのロググループは削除されないので、手動で削除が必要です。
> npx cdk destroy Are you sure you want to delete: AppStack (y/n)? y AppStack: destroying... 11:25:08 | DELETE_IN_PROGRESS | AWS::CloudFormation::Stack | AppStack 11:28:49 | DELETE_IN_PROGRESS | AWS::NetworkFirewall::RuleGroup | NetworkFirewallRuleGroup ✅ AppStack: destroyed
課金のお話
Network Firewallの料金は以下の通りです。詳細は公式ドキュメントをご覧ください。
Resource Type | Price |
---|---|
Network Firewall Endpoint | $0.395/hr |
Network Firewall Traffic Processing | $0.065/GB |
NAT gateway Pricing | Use one hour & one GB of NAT gateway at no additional cost for every hour & GB charged for Network Firewall endpoints. |
AZ単位で課金が発生するので1AZでも1ヶ月フル(24時間×30日)で使うとなると$284.4程度かかります。通常Multi-AZで運用すると思うので、$568.8
と案外なお値段です。
私は検証でかなりの時間格闘したので、Network Firewallだけで$32.09
ほどかかってしまいました...
皆さんも検証用途で作成される際は、削除忘れにお気をつけください。
Network Firewallを使ってみよう!
Network Firewallを使うことによって今までできなかった細かいアクセス制御ができそうです!
お値段は少々しますが、マネージドサービスなのでプロキシサーバの可用性やOSの運用コストなどが減るので、実運用する身としてはかなり助かります。
今回はドメインフィルタリングとありそうでなかった機能の検証でしたが、今度はIPS機能の記事でも書いてみようと思います。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!